package game;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.InputMismatchException;
import java.util.Scanner;

import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.util.BufferedImageUtil;

import player.Player;
import utility.editor.AsciiLevel;
import drawables.EffectUtils;
import drawables.GaussianBlurFilter;
import drawables.GlowFilter;
import drawables.WallDrawable;
import enemy.Enemy;
import enemy.actions.Action;
import enemy.actions.GoToWaypoint;
import enemy.actions.Rotate;
import enemy.actions.Wait;

public class Level implements drawables.Drawable<Level>{
	
	private static final int LIGHT_RADIUS = 50, LIGHT_FALLOFF = 150, EXIT_RADIUS = 35;
	private int width = 600, height = 600;
	private int exitX, exitY;
	
	private ArrayList<WallDrawable> walls;
	private ArrayList<Enemy> enemies = new ArrayList<Enemy>();
	private Player player;
	
	private float[] lightIntensityMap;
	private Image levelBackground;
	private Image exitImage = new Image("resources/board/exit.png");
	
	private boolean levelCompleted, levelFailed;
	
	public Level(File f) throws IOException, SlickException{
		StringBuilder sb = new StringBuilder();
		Scanner scan = new Scanner(f);
		while(scan.hasNext()){
			sb.append(scan.nextLine() + "\n");
		}
		
		construct(sb.toString());
	}
	
	public Level(AsciiLevel level) throws IOException, SlickException{
		StringBuilder sb = new StringBuilder();
		sb.append("[Dimensions]\n");
		sb.append(level.width + "\t");
		sb.append(level.height + "\n");
		sb.append("[Walls]\n");
		for(String s : level.walls)
			sb.append(s + "\n");
		sb.append("[Lights]\n");
		for(String s : level.lights)
			sb.append(s + "\n");
		
		construct(sb.toString());
	}
	
	private void construct(String level) throws IOException, SlickException{
		//read level
		Scanner scanLevel = new Scanner(level);
		walls = new ArrayList<WallDrawable>();
		ArrayList<Point> lights = new ArrayList<Point>();
		while(scanLevel.hasNext()){
			String next = scanLevel.next();
			if(next.equals("[Dimensions]")){
				width = scanLevel.nextInt();
				height = scanLevel.nextInt();
				
				player = new Player(scanLevel.nextInt(), scanLevel.nextInt());
				
				exitX = scanLevel.nextInt();
				exitY = scanLevel.nextInt();
			} else if(next.equals("[Walls]")){
				WallDrawable n, s, e, w;
				n = new WallDrawable(0, 0, 0, height);
				s = new WallDrawable(0, 0, width, 0);
				e = new WallDrawable(width, 0, width, height);
				w = new WallDrawable(0, height, width, height);
				walls.add(n);
				walls.add(s);
				walls.add(e);
				walls.add(w);
				
				while(scanLevel.hasNextInt()){
					WallDrawable wall = new WallDrawable(scanLevel.nextInt(), scanLevel.nextInt(), 
									scanLevel.nextInt(), scanLevel.nextInt());
					walls.add(wall);
				}
			} else if(next.equals("[Lights]")){
				while(scanLevel.hasNextInt()){
					int x = scanLevel.nextInt();
					int y = scanLevel.nextInt();
					lights.add(new Point(x, y));
				}
			} else if(next.equals("[Enemy]")){
				Enemy e = new Enemy(scanLevel.nextFloat(), scanLevel.nextFloat(), scanLevel.nextFloat());
				next = scanLevel.next();
				while(!"END".equalsIgnoreCase(next)){
					Action a;
					if("GOTO".equalsIgnoreCase(next)){
						a = new GoToWaypoint(scanLevel.nextInt(), scanLevel.nextInt());
					} else if("TURN".equalsIgnoreCase(next)){
						next = scanLevel.next();
						boolean right;
						if("LEFT".equalsIgnoreCase(next)){
							right = false;
						} else if("RIGHT".equalsIgnoreCase(next)){
							right = true;
						} else {
							throw new InputMismatchException("Level file is borked!");
						}
						
						a = new Rotate(right, scanLevel.nextFloat());
					} else if("WAIT".equalsIgnoreCase(next)){
						a = new Wait(scanLevel.nextInt());
					} else {
						throw new InputMismatchException("Level file is borked!");
					}
					e.addAction(a);
					next = scanLevel.next();
				}
				enemies.add(e);
			}
		}
		scanLevel.close();
		
		//draw dark map
		BufferedImage darkMap = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		Graphics2D g2d = darkMap.createGraphics();
		EffectUtils.enableAntialias(g2d);
		for(WallDrawable wall : walls)
			wall.draw(g2d);
		
		if(lights.isEmpty()){
			levelBackground = new Image(BufferedImageUtil.getTexture("level_background", darkMap));
			return;
		}
		
		//draw glow map
		BufferedImage glowMap = generateGlowMap(darkMap);
		
		//draw light map for each light then combine
		BufferedImage[] lightMaps = new BufferedImage[lights.size()];
		for(int i=0; i<lights.size(); i++)
			lightMaps[i] = generateLightMap(walls, lights.get(i));
		BufferedImage lightImage = EffectUtils.addImages(lightMaps);
		convertToLightIntensityMap(lightImage);
		
		//composite
		BufferedImage levelMap = EffectUtils.maskImage(darkMap, glowMap, lightIntensityMap);
		//write to file
		levelBackground = new Image(BufferedImageUtil.getTexture("level_background", levelMap));
	}
	
	public void draw(Graphics g, Camera camera){
		g.drawImage(levelBackground, camera.getOffsetX(), camera.getOffsetY());
		g.setDrawMode(Graphics.MODE_ADD);
		g.drawImage(exitImage, exitX + camera.getOffsetX() - (exitImage.getWidth()/2), exitY + camera.getOffsetY() - (exitImage.getHeight()/2));
		g.setDrawMode(Graphics.MODE_NORMAL);
		player.draw(g, camera);
		for(Enemy enemy : enemies){
			enemy.draw(g, camera);
		}
	}
	
	
	
	public float getLightIntensity(int x, int y){
		if(x < 0 || y < 0 || x >= width || y >= height)
			return 0;
		if(lightIntensityMap == null)
			return 0;
		return lightIntensityMap[y * width + x];
	}
	
	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}
	
	public Player getPlayer(){
		return player;
	}

	public ArrayList<Enemy> getEnemies() {
		return enemies;
	}
	
	public boolean collides(Player player){
		Rectangle2D bounds = player.getBoundingBox();
		for(WallDrawable wall : walls){
			if(wall.intersects(bounds))
				return true;
		}

		return false;
	}
	
	public boolean playerAtExit(){
		double dist = Math.hypot(exitX - player.getX(), exitY - player.getY());
		if(dist <= EXIT_RADIUS){
			levelCompleted = true;
			return true;
		} else{
			return false;
		}
	}
	
	public boolean levelCompleted(){
		return levelCompleted;
	}
	
	public boolean levelFailed(){
		return levelFailed;
	}
	
	public void fail(){
		AudioEngine.stopBackgroundMusic();
		levelFailed = true;
	}
	
	public Player canSight(Enemy enemy){
		float dX = player.getX() - enemy.getX();
		float dY = player.getY() - enemy.getY();
		if(Math.hypot(dX, dY) > Enemy.MAX_VISION)//out of range
			return null;
		double angle = Math.atan2(dX, -dY);
		double angleDiff = angle - Math.toRadians(enemy.getRotation());
		if(angleDiff < -Math.PI)
			angleDiff = 2*Math.PI + angleDiff;
		else if(angleDiff > Math.PI)
			angleDiff = angleDiff - 2*Math.PI;
		if(Math.toDegrees(Math.abs(angleDiff)) <= Enemy.FOV){
			//ray trace
			if(Math.abs(dY) > Math.abs(dX)){
				double gradient = dX/dY;
				double x = 0;
				float start = 0, end = 0;
				if(player.getY() < enemy.getY()){
					start = player.getY();
					end = enemy.getY();
					x = player.getX();
				} else{
					start = enemy.getY();
					end = player.getY();
					x = enemy.getX();
				}
				for(float y=start; y<=end; y+=3, x+=gradient*3){
					for(WallDrawable wall : walls)
						if(wall.intersects((float)x, y))
							return null;
				}
			} else{//iterate through x
				double gradient = dY/dX;
				double y = 0;
				float start = 0, end = 0;
				if(player.getX() < enemy.getX()){
					start = player.getX();
					end = enemy.getX();
					y = player.getY();
				} else{
					start = enemy.getX();
					end = player.getX();
					y = enemy.getY();
				}
				for(float x=start; x<=end; x+=3, y+=gradient*3){
					for(WallDrawable wall : walls)
						if(wall.intersects(x, (float)y))
							return null;
				}
			}
			//clear sight
			return player;
		} else
			return null;
	}

	private void convertToLightIntensityMap(BufferedImage lightImage){
		int[] pixels = EffectUtils.getPixels(lightImage);
		lightIntensityMap = new float[pixels.length];
		for(int i=0; i<pixels.length; i++){
			lightIntensityMap[i] = (pixels[i] & 0x000000FF)/(float)255;
		}
	}
	
	private BufferedImage generateGlowMap(BufferedImage darkMap){
		GlowFilter filter = new GlowFilter();
		filter.setHue(0.05f);
		filter.setBaseColor(new Color(0, 75, 150).getRGB());
		return filter.filter(darkMap, null);
	}
	
	private BufferedImage generateLightMap(ArrayList<WallDrawable> walls, Point light){
		//create un-occluded light image
		BufferedImage lightImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		int[] pixels = EffectUtils.getPixels(lightImage);
		for(int y=0; y<height; y++){
			for(int x=0; x<width; x++){
				double dist = Math.hypot(light.x - x, light.y - y);
				int inten = 0;
				if(dist < LIGHT_RADIUS)
					inten = 255;
				else if(dist < LIGHT_FALLOFF + LIGHT_RADIUS){
					double sc = (double)(dist - LIGHT_RADIUS)/LIGHT_FALLOFF;
					inten = (int)(255*(1-sc));
				}
				pixels[y * width + x] = new Color(inten, inten, inten).getRGB();
			}
		}
		EffectUtils.setPixels(pixels, lightImage);
		Graphics2D g2d = lightImage.createGraphics();
		EffectUtils.enableAntialias(g2d);
		g2d.setColor(Color.BLACK);
		//create shadows
		for(WallDrawable wall : walls){
			//project/ray trace
			float[] p1 = projectToEdge(light.x, light.y, wall.startX, wall.startY);
			float[] p2 = projectToEdge(light.x, light.y, wall.endX, wall.endY);
			float angle = angle(light.x, light.y, p1[0], p1[1], p2[0], p2[1]);
			if(angle < 0){
				float[] temp = p1;
				p1 = p2;
				p2 = temp;
				wall.switchEnds();
			}
			float[] center = projectToEdge(light.x, light.y, 
							(wall.startX+wall.endX)/2, (wall.startY+wall.endY)/2);
			//trace shadow path
			GeneralPath path1 = new GeneralPath();
			path1.moveTo(wall.startX, wall.startY);
			path1.lineTo(p1[0], p1[1]);
			//include image corners
			if(center[0] == 0){//left
				path1.lineTo(0, height);
				path1.lineTo(0, 0);
			} else if(center[1] == 0){//up
				path1.lineTo(0, 0);
				path1.lineTo(width, 0);
			} else if(center[0] == width){//right
				path1.lineTo(width, 0);
				path1.lineTo(width, height);
			} else{//down
				path1.lineTo(width, height);
				path1.lineTo(0, height);
			}
			path1.lineTo(p2[0], p2[1]);
			path1.lineTo(wall.endX, wall.endY);
			//draw shadow
			g2d.fill(path1);
		}
		
		//blur shadows
		lightImage = new GaussianBlurFilter(3, 3).filter(lightImage, null);
		
		//create light map
		return lightImage;
	}
	
	private float[] projectToEdge(int fromX, int fromY, int toX, int toY){
		int dX = toX - fromX;
		int dY = toY - fromY;
		if(dY < 0 && -dY >= Math.abs(dX)){//project up
			float gradient = (float)dX/dY;
			float x = fromX + (-fromY)*gradient;
			return new float[]{x, 0};
		} else if(dX > 0 && dX > Math.abs(dY)){//project right
			float gradient = (float)dY/dX;
			float y = fromY + (width-fromX)*gradient;
			return new float[]{width, y};
		} else if(dY > 0 && dY >= Math.abs(dX)){//project down
			float gradient = (float)dX/dY;
			float x = fromX + (height-fromY)*gradient;
			return new float[]{x, height};
		} else{//project left
			float gradient = (float)dY/dX;
			float y = fromY + (-fromX)*gradient;
			return new float[]{0, y};
		}
	}
	
	private float angle(float cX, float cY, float x1, float y1, float x2, float y2){
		float a1 = angle(cX, cY, x1, y1);
		float a2 = angle(cX, cY, x2, y2);
		float diff = a2 - a1;
		if(diff > Math.PI || diff < -Math.PI)
			return -diff;
		return diff;
	}
	
	private static float angle(float x1, float y1, float x2, float y2){
		float dX = x2 - x1;
		float dY = y2 - y1;
		if(dX >= 0 && dY < 0)
			return (float)Math.atan(dX/(-dY));
		else if(dX >= 0)
			return (float)Math.atan(dY/dX) + (float)Math.PI/2;
		else if(dX < 0 && dY >= 0)
			return (float)Math.atan((-dX)/dY) + (float)Math.PI;
		else
			return (float)Math.atan(dY/dX) + (float)Math.PI*3/2;
	}

	@Override
	public void update(Level level) {
		throw new UnsupportedOperationException("Levels can not change after being loaded");
	}
}
